﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Drawing;

namespace Smash
{
    public class VideoReceiver: IDisposable
    {
        public delegate void ImageUpdated();
        public event ImageUpdated OnImageUpdated;

        public delegate void VideoError(string message);
        public event VideoError OnVideoError;

        public delegate void DataReceived(int received);
        public event DataReceived OnDataReceived;

        private class VideoReceiverStatus
        {
            public struct VideoReceiverData
            {
                public byte[] data;
            }

            public bool Error
            {
                get;
                set;
            }

            public bool Finished
            {
                get
                {
                    foreach (VideoReceiverData vrd in Data)
                    {
                        if (vrd.data == null)
                        {
                            return false;
                        }
                    }
                    return true;
                }
            }

            public bool Presented
            {
                get;
                set;
            }

            public ushort Count
            {
                get
                {
                    return count_;
                }
            }

            public ushort Handle
            {
                get
                {
                    return handle_;
                }
            }

            public MemoryStream DataStream
            {
                get
                {
                    if (Finished)
                    {
                        List<byte> bytes = new List<byte>();
                        foreach(VideoReceiverData data in Data)
                        {
                            bytes.AddRange(data.data);
                        }
                        return new MemoryStream(bytes.ToArray());
                    }
                    return null;
                }
            }

            public VideoReceiverData[] Data
            {
                get
                {
                    return data_;
                }
            }

            static public VideoReceiverStatus CreateStatus(byte[] packet,
                                                           out ushort out_index,
                                                           out byte[] out_data)
            {
                out_index = 0;
                out_data = null;
                ushort index = 
                  (ushort)IPAddress.NetworkToHostOrder(
                    (short)BitConverter.ToUInt16(packet, 0));
                ushort count =
                  (ushort)IPAddress.NetworkToHostOrder(
                    (short)BitConverter.ToUInt16(packet, 2));
                ushort handle =
                  (ushort)IPAddress.NetworkToHostOrder(
                    (short)BitConverter.ToUInt16(packet, 4));
                if (index >= count)
                {
                    return null;
                }
                ushort size = 
                  (ushort)IPAddress.NetworkToHostOrder(
                    (short)BitConverter.ToUInt16(packet, 6));
                if (size > MaxVideoDataSize)
                {
                    return null;
                }
                byte[] data = new byte[size];
                Array.Copy(packet, 8, data, 0, size);
                out_index = index;
                out_data = data;
                VideoReceiverStatus status = new VideoReceiverStatus(handle,
                                                                     count);
                status.SetData(index, data);
                return status;
            }

            public VideoReceiverStatus(ushort handle, ushort count)
            {
                Error = false;
                Presented = false;
                count_ = count;
                handle_ = handle;
                data_ = new VideoReceiverData[count];
            }

            public void SetData(ushort index, byte[] data)
            {
                byte[] b = data;
                data_[index].data = b;
            }

            public override string ToString()
            {
                return "Status Count: " + Count
                       + " Handle: " + Handle
                       + " Finished: " + Finished
                       + " Presented: " + Presented;
            }

            private ushort count_;
            private ushort handle_;
            private VideoReceiverData[] data_;
        }

        public static int VideoPacketLength
        {
            get
            {
                return 1032;
            }
        }

        public static int MaxVideoDataSize
        {
            get
            {
                return 1024;
            }
        }

        public bool ServerRunning
        {
            get
            {
                return server_running_;
            }
        }

        public int Port
        {
            get
            {
                return port_;
            }
        }

        public int TimeOut
        {
            get;
            set;
        }

        public long DataReceivedTotal
        {
            get;
            set;
        }
        
        public IPAddress IPAddress
        {
            get
            {
                return address_;
            }
        }

        public IPEndPoint ReceiverEndPoint
        {
            get
            {
                return new IPEndPoint(IPAddress, Port);
            }
        }

        public Image VideoImage
        {
            get;
            set;
        }

        public VideoReceiver(IPAddress address, int port)
        {
            port_ = port;
            address_ = address;
            TimeOut = 250;
            JpegStreams = new Stack<MemoryStream>();
            ReceiverSocket = new Socket(AddressFamily.InterNetwork,
                                        SocketType.Dgram,
                                        ProtocolType.Udp);
            EndPoint endPoint = ReceiverEndPoint;
            ReceiverSocket.Bind(endPoint);
            ReceiverSocket.ReceiveTimeout = 250;
            VideoBuffer = new byte[VideoPacketLength];
            VideoBufferRead = 0;
            JpegStreamThread = new Thread(ImageUpdateHelper);
            JpegStreamThread.Start();
            receive_finish_mutex_ = new object();
            Log = new StreamWriter("receiver-log.log");
        }

        public void Start()
        {
            ResetStatus(true);
            EndPoint endPointReceive = ReceiverEndPoint;
            ReceiverSocket.ReceiveBufferSize = 1048576;
            ReceiverSocket.BeginReceiveFrom(VideoBuffer, 0,
                                            VideoBuffer.Length,
                                            SocketFlags.None,
                                            ref endPointReceive,
                                            ReceivePacket,
                                            ReceiverSocket);
        }

        public void Stop()
        {
            ResetStatus(false);            
        }

        public void Dispose()
        {
            Stop();
            JpegStreamThread.Abort();
            Log.Close();
        }

        private void ProcVideoError(string message)
        {
            if (OnVideoError != null)
            {
                OnVideoError(message);
                if (message != null)
                {
                    Log.WriteLine(message);
                }
            }
        }

        private void ProcImageUpdate()
        {
            if (OnImageUpdated != null)
            {
                OnImageUpdated();
            }
            else
            {
                ProcVideoError("Could not invoke new video image!");
            }
        }

        private void ProcDataReceived(int received)
        {
            if (OnDataReceived != null)
            {
                OnDataReceived(received);
            }
            else
            {
                ProcVideoError("Could not invoke data received");
            }
        }

        private void ImageUpdateHelper()
        {
            while (true)
            {
                if (ServerRunning)
                {
                    MemoryStream jpegStream;
                    if (JpegStreams.Count > 0)
                    {
                        lock (JpegStreams)
                        {
                            jpegStream = JpegStreams.Pop();
                            JpegStreams.Clear();
                        }
                        try
                        {
                            VideoImage = Image.FromStream(jpegStream);
                        }
                        catch (Exception ex)
                        {
                        }
                        ProcImageUpdate();
                    }
                    Thread.Sleep(33);
                }
            }
        }

        private void ReceivePacket(IAsyncResult result)
        {            
            if (ServerRunning)
            {
                Socket receiver = (Socket)result.AsyncState;
                EndPoint endPointReceive = ReceiverEndPoint;
                int received = receiver.EndReceiveFrom(result,
                                                       ref endPointReceive);
                ReceivedData(received);
                if (VideoBufferRead < VideoPacketLength)
                {
                    ReceiverSocket.BeginReceiveFrom(VideoBuffer, 
                                                    VideoBufferRead,
                                                    VideoPacketLength
                                                      - VideoBufferRead,
                                                    SocketFlags.None,
                                                    ref endPointReceive,
                                                    ReceivePacket,
                                                    ReceiverSocket);
                }
                else
                {
                    byte[] packet = VideoBuffer;
                    VideoBuffer = new byte[VideoPacketLength];
                    VideoBufferRead = 0;
                    FinishPacketReceive(packet);
                    ReceiverSocket.BeginReceiveFrom(VideoBuffer,
                                                    VideoBufferRead,
                                                    VideoPacketLength
                                                      - VideoBufferRead,
                                                    SocketFlags.None,
                                                    ref endPointReceive,
                                                    ReceivePacket,
                                                    ReceiverSocket);
                }
            }
            else
            {
                ReceiverSocket.Close(0);
            }
        }

        private void FinishPacketReceive(byte[] packet)
        {
            lock (receive_finish_mutex_)
            {
                try
                {
                    ushort index;
                    byte[] data;
                    VideoReceiverStatus status =
                      VideoReceiverStatus.CreateStatus(packet, 
                                                       out index, out data);
                    bool force_status_update =
                      (DateTime.Now - LastPacketReceivedTime)
                        .TotalMilliseconds >= TimeOut;
                    LastPacketReceivedTime = DateTime.Now;
                    if (Status == null || force_status_update)
                    {
                        ProcVideoError(null);
                        Status = status;
                        OldStatus = null;
                    }
                    else if (status != null)
                    {
                        ProcVideoError(null);
                        if (TryUpdateStatus(Status, status, index, data)) ;
                        else if (TryUpdateStatus(OldStatus, status,
                                                 index, data)) ;
                        else if ((ushort)(Status.Handle + 1) == status.Handle)
                        {
                            OldStatus = Status;
                            Status = status;
                        }
                        else
                        {
                            ProcVideoError("Out of Sequence Packet "
                                           + "expected "
                                           + (ushort)(Status.Handle + 1)
                                           + " found " + status.Handle);
                        }
                    }
                    else
                    {
                        ProcVideoError("Malformed Packet");
                    }
                }
                catch(Exception ex)
                {
                    // caught a random error here
                    // null pointer perhaps due to Status being null
                    // it can happen with concurrent programming
                }
            }
        }

        private bool TryUpdateStatus(VideoReceiverStatus old_status,
                                     VideoReceiverStatus new_status,
                                     ushort index,
                                     byte[] data)
        {
            if (old_status != null)
            {
                if (old_status.Handle == new_status.Handle)
                {
                    if (old_status.Presented)
                    {
                        ProcVideoError("Received faulty packet for Handle "
                            + new_status.Handle);
                    }
                    else
                    {
                        old_status.SetData(index, data);
                    }
                    lock (JpegStreams)
                    {
                        MemoryStream jpegStream = old_status.DataStream;
                        if (jpegStream != null)
                        {
                            JpegStreams.Push(jpegStream);
                            old_status.Presented = true;
                        }
                    }
                    return true;
                }
            }
            return false;
        }

        private void ReceivedData(int received)
        {
            VideoBufferRead += received;
            DataReceivedTotal += received;
            ProcDataReceived(received);
        }

        private void ResetStatus(bool running)
        {
            server_running_ = running;
            Status = null;
            OldStatus = null;            
            VideoImage = null;            
            VideoBufferRead = 0;
            VideoBuffer = new byte[VideoPacketLength];
            JpegStreams.Clear();
            OnImageUpdated();
        }

        private byte[] VideoBuffer
        {
            get;
            set;
        }

        private int VideoBufferRead
        {
            get;
            set;
        }

        private Thread JpegStreamThread
        {
            get;
            set;
        }

        private Socket ReceiverSocket
        {
            get;
            set;
        }

        private StreamWriter Log
        {
            get;
            set;
        }

        private Stack<MemoryStream> JpegStreams
        {
            get;
            set;
        }

        private VideoReceiverStatus OldStatus
        {
            get;
            set;
        }

        private VideoReceiverStatus Status
        {
            get;
            set;
        }

        private DateTime LastPacketReceivedTime
        {
            get;
            set;
        }

        private object receive_finish_mutex_;
        private bool server_running_;
        private int port_;
        private IPAddress address_;
    }
}
